﻿''' <summary>
''' Provides a base class to host command processing methods and functionality to execute commands from a line of text.
''' </summary>
Public Class CommandProcessor
    Public Event CommandExecutionStarted As EventHandler(Of CommandExecutionEventArgs)
    Public Event CommandExecutionEnded As EventHandler(Of CommandExecutionEventArgs)
    Public Event CommandExecutionError As EventHandler(Of CommandExecutionErrorEventArgs)

    ''' <summary>
    ''' Gets the last exception generated when executing a command.  Access this property when a command
    ''' returns the EXCEPTION result status.
    ''' </summary>
    ''' <returns></returns>
    Public ReadOnly Property LastCommandError As Exception

    Private commands As New Dictionary(Of String, Action(Of CommandLine))
    Private keywords As New SearchableStringCollection
    Private alternates As New Dictionary(Of String, String)
    Private helpText As New Dictionary(Of String, CommandHelpInfo)
    Protected lastCustomStatus As String

    Public Sub New()
        InitCommandsFromObject(Me)
    End Sub

    Public Sub New(primaryCommandObject As Object)
        InitCommandsFromObject(primaryCommandObject)
    End Sub

    Public Function ContainsCommand(command As String) As Boolean
        Return commands.ContainsKey(command)
    End Function

    ''' <summary>
    ''' Parses the specified line of text into a CommandLine instance, then attempts to execute the command.
    ''' This method returns a status value indicating the result of attempting to execute the command.
    ''' </summary>
    ''' <param name="commandLine">The string command line to parse and execute.</param>
    ''' <returns>The status result from executing the command.</returns>
    Public Function ExecuteCommand(commandLine As String) As CommandExecutionResultStatus
        _LastCommandError = Nothing
        lastCustomStatus = Nothing
        Dim line As CommandLine = Nothing
        Dim result As CommandExecutionResultStatus = CommandExecutionResultStatus.OK
        Try
            line = New CommandLine(commandLine)
            OnCommandStarted(New CommandExecutionEventArgs(line, result))
            Dim commandWord As String = Nothing

            If keywords.Contains(line.Command) Then
                commandWord = line.Command
                If Not commands.ContainsKey(commandWord) Then commandWord = alternates(commandWord)
            Else
                Dim matches() = keywords.Find(line.Command)
                If matches.Length = 1 Then
                    commandWord = matches(0)
                    If Not commands.ContainsKey(commandWord) Then commandWord = alternates(commandWord)
                ElseIf matches.Length > 1 Then
                    result = CommandExecutionResultStatus.AMBIGUOUS
                Else
                    result = CommandExecutionResultStatus.UNKNOWN
                End If
            End If

            If Not String.IsNullOrEmpty(commandWord) Then
                Try
                    commands(commandWord)(line)
                    If Not String.IsNullOrEmpty(line.CustomStatus) Then
                        result = CommandExecutionResultStatus.CUSTOM
                        lastCustomStatus = line.CustomStatus
                    End If
                Catch ex As Exception
                    result = CommandExecutionResultStatus.EXCEPTION
                    OnCommandExecutionError(New CommandExecutionErrorEventArgs(line, ex, result))
                End Try
            End If
        Catch ex As Exception
            result = CommandExecutionResultStatus.INVALID
            OnCommandExecutionError(New CommandExecutionErrorEventArgs(commandLine, ex, result))
        End Try
        If line Is Nothing Then
            OnCommandExecutionEnded(New CommandExecutionEventArgs(commandLine, result))
        Else
            OnCommandExecutionEnded(New CommandExecutionEventArgs(line, result))
        End If
        Return result
    End Function

    Public Function GetAlternates(command As String) As IEnumerable(Of String)
        Dim result As New List(Of String)
        For Each kvp In alternates
            If kvp.Value = command Then result.Add(kvp.Key)
        Next
        Return result.AsEnumerable
    End Function
    ''' <summary>
    ''' Gets the help caption associated with the specified command keyword.
    ''' </summary>
    ''' <param name="command">The name of the command whose help caption sould be retrieved.</param>
    ''' <returns></returns>
    Public Function GetCommandHelpCaption(command As String) As String
        command = command.ToLower
        Dim commandWord As String = Nothing
        If keywords.Contains(command) Then
            commandWord = command
        Else
            Dim matches() = keywords.Find(command)
            If matches.Length = 1 Then
                commandWord = matches(0)
                If Not commands.ContainsKey(commandWord) Then commandWord = alternates(commandWord)
            End If
        End If
        If Not String.IsNullOrEmpty(commandWord) Then Return helpText(commandWord).Caption
        Return Nothing
    End Function

    ''' <summary>
    ''' Gets the help description associated with the specified command keyword.
    ''' </summary>
    ''' <param name="command">The name of the command whose help description sould be retrieved.</param>
    ''' <returns></returns>
    Public Function GetCommandHelpDescription(command As String) As String
        command = command.ToLower
        Dim commandWord As String = Nothing
        If keywords.Contains(command) Then
            commandWord = command
        Else
            Dim matches() = keywords.Find(command)
            If matches.Length = 1 Then
                commandWord = matches(0)
                If Not commands.ContainsKey(commandWord) Then commandWord = alternates(commandWord)
            End If
        End If
        If Not String.IsNullOrEmpty(commandWord) Then Return helpText(commandWord).Description
        Return Nothing
    End Function

    Public Function GetCommandKeywords() As IEnumerable(Of String)
        Return commands.Keys.AsEnumerable
    End Function

    ''' <summary>
    ''' Initializes command methods declared on the specified object instance.
    ''' </summary>
    ''' <param name="obj">The object containing command methods.</param>
    Public Sub InitCommandsFromObject(obj As Object)
        Dim ttype As Type = obj.GetType
        For Each method In ttype.GetMethods(Reflection.BindingFlags.Instance Or Reflection.BindingFlags.NonPublic Or Reflection.BindingFlags.Public)
            Dim attribs() = method.GetCustomAttributes(GetType(CommandMethod), True)
            If attribs.Length = 1 Then
                Dim params() = method.GetParameters
                If params.Length = 1 AndAlso params(0).ParameterType Is GetType(CommandLine) Then
                    If method.ReturnType Is GetType(Void) Then
                        Dim commandAttrib As CommandMethod = CType(attribs(0), CommandMethod)
                        Dim commandWord As String = method.Name.ToLower
                        keywords.Add(commandWord)
                        If Not String.IsNullOrEmpty(commandAttrib.AlternateNames) Then
                            Dim altNames() As String = commandAttrib.AlternateNames.Split(",")
                            For Each altName In altNames
                                altName = altName.Trim.ToLower
                                alternates(altName) = commandWord
                                keywords.Add(altName)
                            Next
                        End If
                        helpText(commandWord) = New CommandHelpInfo(commandAttrib)
                        commands(commandWord) = CType(method.CreateDelegate(GetType(Action(Of CommandLine)), obj), Action(Of CommandLine))
                    End If
                End If
            End If
        Next
    End Sub

    Protected Overridable Sub OnCommandExecutionEnded(e As CommandExecutionEventArgs)
        RaiseEvent CommandExecutionEnded(Me, e)
    End Sub

    Protected Overridable Sub OnCommandExecutionError(e As CommandExecutionErrorEventArgs)
        _LastCommandError = e.Exception
        RaiseEvent CommandExecutionError(Me, e)
    End Sub

    Protected Overridable Sub OnCommandStarted(e As CommandExecutionEventArgs)
        RaiseEvent CommandExecutionStarted(Me, e)
    End Sub

    Public Sub ShowCommandHelp(inWindow As Window, command As String)
        inWindow.AppendText($"Command:  {command.ToUpper}", True)
        inWindow.AppendText("USAGE:", True)
        inWindow.AppendText($"  {GetCommandHelpCaption(command)}", True)
        inWindow.AppendText("DESCRIPTION:", True)
        inWindow.AppendText($"  {GetCommandHelpDescription(command)}", True)
        Dim alts = GetAlternates(command)
        If alts.Count > 0 Then
            inWindow.AppendText("ALTERNATE COMMAND KEYWORDS:", True)
            For Each alt In alts
                inWindow.AppendText($"  {alt.ToUpper}", True)
            Next
        End If
        inWindow.AppendText(ControlChars.NewLine, True)
    End Sub

    Private Class CommandHelpInfo
        Public ReadOnly Property Caption As String
        Public ReadOnly Property Description As String

        Public Sub New(commandAttrib As CommandMethod)
            Caption = commandAttrib.HelpCaption
            Description = commandAttrib.HelpDescription
        End Sub
    End Class

    Public Class CommandExecutionEventArgs
        Inherits EventArgs
        Public ReadOnly CommandLine As CommandLine
        Public ReadOnly CommandString As String
        Public ReadOnly CurrentStatus As CommandExecutionResultStatus

        Protected Friend Sub New(line As CommandLine, status As CommandExecutionResultStatus)
            CommandLine = line
            CommandString = line.GetOriginalCommandLine
            CurrentStatus = status
        End Sub
        Protected Friend Sub New(command As String, status As CommandExecutionResultStatus)
            CommandString = command
            CurrentStatus = status
        End Sub
    End Class

    Public Class CommandExecutionErrorEventArgs
        Inherits CommandExecutionEventArgs
        Public ReadOnly Exception As Exception
        Protected Friend Sub New(line As CommandLine, ex As Exception, status As CommandExecutionResultStatus)
            MyBase.New(line, status)
            Exception = ex
        End Sub
        Protected Friend Sub New(command As String, ex As Exception, status As CommandExecutionResultStatus)
            MyBase.New(command, status)
            Exception = ex
        End Sub
    End Class

    Public Enum CommandExecutionResultStatus
        ''' <summary>
        ''' The command executed successfully.
        ''' </summary>
        OK
        ''' <summary>
        ''' More than one command matched the provided characters.
        ''' </summary>
        AMBIGUOUS
        ''' <summary>
        ''' The command entered was not recognized.
        ''' </summary>
        UNKNOWN
        ''' <summary>
        ''' The command entered has one or more invalid parameters.
        ''' </summary>
        INVALID
        ''' <summary>
        ''' The command encountered an exception during execution.
        ''' </summary>
        EXCEPTION
        ''' <summary>
        ''' The command returned a custom status result.
        ''' </summary>
        CUSTOM
    End Enum
End Class

